import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import Parameter

from .misc import ModuleWrapper


def calculate_kl(log_alpha):
    return 0.5 * torch.sum(torch.log1p(torch.exp(-log_alpha)))


class BBBLinear(ModuleWrapper):
    
    def __init__(self, in_features, out_features, alpha_shape=(1, 1), bias=True, name='BBBLinear'):
        super(BBBLinear, self).__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.alpha_shape = alpha_shape
        self.weight = Parameter(torch.Tensor(out_features, in_features))
        self.log_alpha = Parameter(torch.Tensor(*alpha_shape))
        if bias:
            self.bias = Parameter(torch.Tensor(1, out_features))
        else:
            self.register_parameter('bias', None)
        self.reset_parameters()
        self.kl_value = calculate_kl
        self.name = name

    def reset_parameters(self):
        stdv = 1. / math.sqrt(self.weight.size(1))
        self.weight.data.uniform_(-stdv, stdv)
        self.log_alpha.data.fill_(-5.0)
        if self.bias is not None:
            self.bias.data.zero_()

    def forward(self, x):

        mean = F.linear(x, self.weight)
        if self.bias is not None:
            mean = mean + self.bias

        if self.training:
            sigma = torch.exp(self.log_alpha) * self.weight * self.weight
            std = torch.sqrt(1e-16 + F.linear(x * x, sigma))
            epsilon = std.data.new(std.size()).normal_()
            out = mean + std * epsilon
        else:
            epsilon = 0.0
            out = mean
        # Local reparameterization trick
        return out

    def kl_loss(self):
        return [self.kl_value(self.log_alpha)]

